/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package instantmessengercpsc360.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author nicholas
 */
public class ClientWrapper implements Runnable, SendMessageEventListener, 
                                                ConvUpdateEventListener,
                                                OpRecvEventListener
    {
    public ClientWrapper(ClientManager parent, Socket sock) throws IOException {
        this.skt = sock;
        out = new PrintWriter(skt.getOutputStream(), true);
        in = new BufferedReader(new InputStreamReader(skt.getInputStream()));
        running = true;
        mgr = parent;
    }
    
    protected ClientManager mgr;
    protected Socket skt;
    protected PrintWriter out;
    protected BufferedReader in;
    protected String username;
    protected Boolean running;
    
    public LinkedList<String> opQueue = new LinkedList<String>();
    public ReentrantLock opLock = new ReentrantLock();
    
    public LinkedList<OpRecvEventListener> ackQueue = new LinkedList<OpRecvEventListener>();
    public ReentrantLock ackLock = new ReentrantLock();
    
    protected ArrayList<ClientWrapper> invitees = new ArrayList<ClientWrapper>();
    
    protected Conversation conv;
    
    //opcodes + approx descriptions
    protected static final char opconnect    = 'c'; //connects to server
    protected static final char opfail       = 'f'; //last action failed
    protected static final char opdisconnect = 'd'; //disconnects conversation
    protected static final char opterminate  = 'x'; //terminates server connection
    protected static final char opmsg        = 'm'; //sends a message to im partner
    protected static final char opconv       = 'p'; //asks to start/join conversation
    protected static final char opconvgroup  = 'g'; //list of members in conversation
    protected static final char opdisconv    = 'v'; //asks to end conversation
    protected static final char optyping     = 't'; //user is currently typing
    
    /**
     * Sends the message to the client.
     * @param ln message to send
     */
    @Override
    public void SendMessageEventOccurred(SendMessageEvent evt)
    {
        String send = opmsg + evt.getSource().toString() + ": " + evt.getMessage();
        out.println(send);
    }

    @Override
    public void ConvUpdateEventOccurred(ConvUpdateEvent evt) {
        if (evt.getAction() == ConvUpdateEvent.ActionType.add)
        {
            String addall = opconvgroup + "";
            for (ClientWrapper cw : conv.getMembers())
            {
                if (!cw.equals(this))
                    addall += " " + cw.toString();
            }
            out.println(addall);
        }
        else if (evt.getAction() == ConvUpdateEvent.ActionType.remove)
            out.println(opdisconv + evt.getSource().toString());
    }
    
    /**
     * Sends an opcode to say that the user is typing.
     */
    public void sendIsTyping()
    {
        out.write(optyping);
    }
    
    public void addNewClient(String usrname)
    {
        out.println(opconnect + usrname);
    }
    
    /**
     * Sends the disconnect opcode and terminates the socket.
     */
    public void terminate()
    {
        out.println(opterminate);
        running = false;
    }

    @Override
    public void run() {
        mgr.clientsLock.lock();
        while(running) {
            try {
                opLock.lock();
                try {
                    if (!opQueue.isEmpty())
                    {
                        String msg = opQueue.pollFirst();
                        out.println(msg);
                    }
                } finally {
                    opLock.unlock();
                }
                if (in.ready())
                {
                    String s = in.readLine();
                    System.out.println(username+"=>server: "+s);
                    char c = s.charAt(0);
                    s = s.substring(1);
                    switch (c)
                    {
                        case opconnect:
                            //Checks name uniqueness and then sends back client list
                            if (!ServerMainFrame.clientExists(s))
                            {
                                username = s;
                                String reply  = opconnect + "";
                                for (ClientWrapper cw : ServerMainFrame.manager.clients)
                                {
                                    if (cw.getUsername() == null)
                                        continue;
                                    if (!cw.equals(this))
                                        reply += cw.getUsername() + " ";
                                }
                                out.println(reply);
                                fireServerListUpdateEvent(new ServerListUpdateEvent(this, true));
                                mgr.clientsLock.unlock();
                            }
                            else
                                out.println(opfail);
                            break;
                        case opdisconnect:
                            //TODO server killed it?
                            break;
                        case opconv:
                            //person outside conv reaching in, or v.versa, no difference
                            ClientWrapper contact = mgr.getClient(s);
                            sendInvite(contact);
                            break;
                        case opdisconv:
                            if (conv != null)
                            {
                                conv.removeMember(this);
                                conv = null;
                            }
                            break;
                        case opmsg:
                            if (conv != null)
                            {
                                conv.sendMessage(this, s);
                            }
                            break;
                        case opfail:
                            //TODO ackQueue not used anywhere except here
                            ackLock.lock();
                            try {
                                if (!ackQueue.isEmpty())
                                    ackQueue.pollFirst().OpRecvEventOccurred(
                                            new OpRecvEvent(this, this.toString(), opfail));
                            } finally {
                                ackLock.unlock();
                            }
                            break;
                        case optyping:
                            break;
                        case opterminate:
                            fireServerListUpdateEvent(new ServerListUpdateEvent(this, false));
                            running = false;
                            break;
                    }
                }
                else
                    Thread.sleep(300);
            } catch (IOException ex) {
                Logger.getLogger(ClientWrapper.class.getName()).log(Level.SEVERE, null, ex);
            } catch (InterruptedException ex) {
                Logger.getLogger(ClientWrapper.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
        //clean up the connection
        try {
            skt.close();
            ServerMainFrame.manager.clients.remove(this);
        } catch (IOException ex) {
            Logger.getLogger(ClientWrapper.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public String getUsername() {
        return username;
    }

    @Override
    public void OpRecvEventOccurred(OpRecvEvent evt) {
        //TODO check, messages might be lost, errors thrown if a message is sent during this op
        
        char op = evt.getOpCode();
        ClientWrapper other = evt.getSource();
        switch(op)
        {
            case opconv:
                //if both users have no conversation
                if (this.conv == null && other.conv == null)
                {
                    conv = new Conversation(this);
                    other.conv = conv;
                    conv.addMember(other);
                }
                //if other user has no conversation
                else if (conv != null && other.conv == null)
                {
                    conv.writeLock.lock();
                    try {
                        other.conv = conv;
                        conv.addMember(other);
                    } finally {
                        conv.writeLock.unlock();
                    }
                }
                //if this user has no conversation
                else if (this.conv == null && other.conv != null)
                {
                    this.conv = other.conv;
                    conv.addMember(this);
                }
                //both users have conversations going
                else
                {
                    conv.writeLock.lock();
                    try {
                        //add everyone from other to current conversation
                        for (ClientWrapper cw : other.conv.getMembers())
                        {
                            cw.conv = conv;
                            conv.addMember(cw);
                        }
                    } finally {
                        conv.writeLock.unlock();
                    }
                }
                break;
            case opfail:
                out.println(evt.getOpCode() + evt.getMessage());
                break;
        }
    }
    
    //Custom Event Code
    protected javax.swing.event.EventListenerList listenerList =
        new javax.swing.event.EventListenerList();
    public void addServerListUpdateEventListener(ServerListUpdateEventListener listener) {
        listenerList.add(ServerListUpdateEventListener.class, listener);
    }
    public void removeServerListUpdateEventListener(ServerListUpdateEventListener listener) {
        listenerList.remove(ServerListUpdateEventListener.class, listener);
    }

    // This private class is used to fire SliderChangeEvents
    private void fireServerListUpdateEvent(ServerListUpdateEvent evt) {
        Object[] listeners = listenerList.getListenerList();
        // Each listener occupies two elements - the first is the listener class
        // and the second is the listener instance
        for (int i=0; i<listeners.length; i+=2) {
            if (listeners[i]==ServerListUpdateEventListener.class) {
                ((ServerListUpdateEventListener)listeners[i+1]).ServerListUpdateEventOccurred(evt);
            }
        }
    }
    
    //Custom Event Code
    public void addOpRecvEventListener(OpRecvEventListener listener) {
        listenerList.add(OpRecvEventListener.class, listener);
    }
    public void removeOpRecvEventListener(OpRecvEventListener listener) {
        listenerList.remove(OpRecvEventListener.class, listener);
    }

    // This private class is used to fire SliderChangeEvents
    private void fireOpRecvEvent(OpRecvEvent evt) {
        Object[] listeners = listenerList.getListenerList();
        // Each listener occupies two elements - the first is the listener class
        // and the second is the listener instance
        for (int i=0; i<listeners.length; i+=2) {
            if (listeners[i]==OpRecvEventListener.class) {
                ((OpRecvEventListener)listeners[i+1]).OpRecvEventOccurred(evt);
            }
        }
    }
    
    //TODO check to make sure event does not get sent if client has not selected a username

    /**
     * Sends a message to the client to update their client list with the new addition or deletion.
     * @param evt The <code>ServerListUpdateEvent</code> with the client information
     */
    public void postUpdateToClientList(ServerListUpdateEvent evt)
    {
        String reply = "";
        if (evt.isConnecting())
        {
            reply += opconnect + evt.getSource().getUsername();
        }
        else
        {
            reply += opdisconnect + evt.getSource().getUsername();
        }
        out.println(reply);
    }

    @Override
    public String toString() {
        return username == null ? "" : username;
    }

    private void sendInvite(ClientWrapper cw) {
        //already invited, arrange conversation
        if (cw.invitees.contains(this))
        {
            //if both users have no conversation
            if (this.conv == null && cw.conv == null)
            {
                conv = new Conversation(this);
                cw.conv = conv;
                conv.addMember(cw);
            }
            //if other user has no conversation
            else if (conv != null && cw.conv == null)
            {
                conv.writeLock.lock();
                try {
                    cw.conv = conv;
                    conv.addMember(cw);
                } finally {
                    conv.writeLock.unlock();
                }
            }
            //if this user has no conversation
            else if (this.conv == null && cw.conv != null)
            {
                this.conv = cw.conv;
                conv.addMember(this);
            }
            //both users have conversations going
            else
            {
                conv.writeLock.lock();
                try {
                    //add everyone from other to current conversation
                    for (ClientWrapper add : cw.conv.getMembers())
                    {
                        add.conv = conv;
                        conv.addMember(add);
                    }
                } finally {
                    conv.writeLock.unlock();
                }
            }
            cw.invitees.remove(this);
        }
        //not invited, new invite, ask user
        else
        {
            if (conv != null)
            {
                if (conv.isMember(cw))
                {
                    out.println(opfail);
                }
            }
            //dont reinvite them
            else if (!invitees.contains(cw))
            {
                cw.out.println(opconv + username);
                invitees.add(cw);
            }
        }
    }
}
